#include "cron_expression.h"
#include "static_initializer.h"
#include <cctype>
#include <algorithm>
#include <sstream>
#include <boost/tokenizer.hpp>
#include <boost/lexical_cast.hpp>
#include <iostream>

namespace gsf {

	const int CronExpression::SECOND = 0;
	const int CronExpression::MINUTE = 1;
	const int CronExpression::HOUR = 2;
	const int CronExpression::DAY_OF_MONTH = 3;
	const int CronExpression::MONTH = 4;
	const int CronExpression::DAY_OF_WEEK = 5;
	const int CronExpression::YEAR = 6;
	const int CronExpression::ALL_SPEC_INT = 99; // '*'
	const int CronExpression::NO_SPEC_INT = 98; // '?'
	const int CronExpression::ALL_SPEC = ALL_SPEC_INT;
	const int CronExpression::NO_SPEC = NO_SPEC_INT;

	std::map<std::string,int> CronExpression::monthMap;
	std::map<std::string,int> CronExpression::dayMap;

	int CronExpression::MAX_YEAR = 2200;

	static const int SUNDAY = 1;
	static const int MONDAY = 2;
	static const int TUESDAY = 3;
	static const int WEDNESDAY = 4;
	static const int THURSDAY = 5;
	static const int FRIDAY = 6;
	static const int SATURDAY = 7;

	void CronExpression::InternalInit() {
		CronExpression::monthMap["JAN"] = 0;
		CronExpression::monthMap["FEB"] = 1;
		CronExpression::monthMap["MAR"] = 2;
		CronExpression::monthMap["APR"] = 3;
		CronExpression::monthMap["MAY"] = 4;
		CronExpression::monthMap["JUN"] = 5;
		CronExpression::monthMap["JUL"] = 6;
		CronExpression::monthMap["AUG"] = 7;
		CronExpression::monthMap["SEP"] = 8;
		CronExpression::monthMap["OCT"] = 9;
		CronExpression::monthMap["NOV"] = 10;
		CronExpression::monthMap["DEC"] = 11;

		CronExpression::dayMap["SUN"] = 1;
		CronExpression::dayMap["MON"] = 2;
		CronExpression::dayMap["TUE"] = 3;
		CronExpression::dayMap["WED"] = 4;
		CronExpression::dayMap["THU"] = 5;
		CronExpression::dayMap["FRI"] = 6;
		CronExpression::dayMap["SAT"] = 7;
	}
	GSF_STATIC_INITIALIZER(CronExpression::InternalInit());

	std::string str_toupper(std::string s) {
		std::transform(s.begin(), s.end(), s.begin(),
			[](unsigned char c) { return std::toupper(c); } // ÕýÈ·
		);
		return s;
	}

	int skipWhiteSpace(int i, const std::string& s) {
		for (; i < s.length() && (s[i] == ' ' || s[i] == '\t'); i++) {
			;
		}
		return i;
	}

	int findNextWhiteSpace(int i, const std::string& s) {
		for (; i < s.length() && (s[i] != ' ' || s[i] != '\t'); i++) {
			;
		}
		return i;
	}
	
	int getNumericValue(const std::string& s, int i) {
		int endOfVal = findNextWhiteSpace(i, s);
		auto val = s.substr(i, endOfVal);
		return boost::lexical_cast<int>(val);
	}

	bool isLeapYear(int year) {
		return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0));
	}

	int getLastDayOfMonth(int monthNum, int year) {

		switch (monthNum) {
		case 1:
			return 31;
		case 2:
			return (isLeapYear(year)) ? 29 : 28;
		case 3:
			return 31;
		case 4:
			return 30;
		case 5:
			return 31;
		case 6:
			return 30;
		case 7:
			return 31;
		case 8:
			return 31;
		case 9:
			return 30;
		case 10:
			return 31;
		case 11:
			return 30;
		case 12:
			return 31;
		default:
			throw std::string("Illegal month number.");
		}
	}

	ValueSetPtr getValue(int v, const std::string& s, int i) {
		char c = s[i];
		std::string s1;
		s1.append(boost::lexical_cast<std::string>(v));
		while (c >= '0' && c <= '9') {
			s1.append(&c,1);
			i++;
			if (i >= s.length()) {
				break;
			}
			c = s[i];
		}
		ValueSetPtr val = std::make_shared<ValueSet>();

		val->pos = (i < s.length()) ? i : i + 1;
		val->value = boost::lexical_cast<int>(s1);
		return val;
	}

	std::set<int> tailSet(const std::set<int>& set, int start) {
		std::set<int> r;
		for (auto iter = set.lower_bound(start); iter != set.end(); ++iter)
			r.insert(*iter);
		return r;
	}

	CronExpression::CronExpression(const std::string& cronExpression)
	{
		this->mCronExpression = str_toupper(cronExpression);
		BuildExpression();
	}

	std::set<int>& CronExpression::GetSet(int type) {

		static std::set<int> kEmpty;

		switch (type) {
		case SECOND:
			return mSeconds;
		case MINUTE:
			return mMinutes;
		case HOUR:
			return mHours;
		case DAY_OF_MONTH:
			return mDaysOfMonth;
		case MONTH:
			return mMonths;
		case DAY_OF_WEEK:
			return mDaysOfWeek;
		case YEAR:
			return mYears;
		default:
			return kEmpty;
		}
	}

	void CronExpression::CheckIncrementRange(int incr, int type, int idxPos) {
		if (incr > 59 && (type == SECOND || type == MINUTE)) {
			throw std::string("Increment > 60 : ");
		}
		else if (incr > 23 && (type == HOUR)) {
			throw std::string("Increment > 24 : ");
		}
		else if (incr > 31 && (type == DAY_OF_MONTH)) {
			throw std::string("Increment > 31 : ");
		}
		else if (incr > 7 && (type == DAY_OF_WEEK)) {
			throw std::string("Increment > 7 : ");
		}
		else if (incr > 12 && (type == MONTH)) {
			throw std::string("Increment > 12 : ");
		}
	}

	int CronExpression::GetMonthNumber(const std::string& s) {
		auto iter = monthMap.find(s);
		if (iter == monthMap.end())
			return -1;
		return iter->second;
	}

	int CronExpression::GetDayOfWeekNumber(const std::string& s) {
		auto iter = dayMap.find(s);
		if (iter == dayMap.end())
			return -1;
		return iter->second;
	}

	int CronExpression::StoreExpressionVals(int pos, std::string s, int type)
	{
		int incr = 0;
		int i = skipWhiteSpace(pos, s);
		if (i >= s.length()) {
			return i;
		}
		char c = s[i];
		if ((c >= 'A') && (c <= 'Z') && (s != "L") && (s != "LW")/* && (!s.matches("^L-[0-9]*[W]?"))*/) {
			auto sub = s.substr(i, i + 3);
			int sval = -1;
			int eval = -1;
			if (type == MONTH) {
				sval = GetMonthNumber(sub) + 1;
				if (sval <= 0) {
					throw std::string("Invalid Month value: '" + sub + "'");
				}
				if (s.length() > i + 3) {
					c = s[i + 3];
					if (c == '-') {
						i += 4;
						sub = s.substr(i, i + 3);
						eval = GetMonthNumber(sub) + 1;
						if (eval <= 0) {
							throw std::string("Invalid Month value: '" + sub + "'", i);
						}
					}
				}
			}
			else if (type == DAY_OF_WEEK) {
				sval = GetDayOfWeekNumber(sub);
				if (sval < 0) {
					throw std::string("Invalid Day-of-Week value: '" + sub + "'");
				}
				if (s.length() > i + 3) {
					c = s[i + 3];
					if (c == '-') {
						i += 4;
						sub = s.substr(i, i + 3);
						eval = GetDayOfWeekNumber(sub);
						if (eval < 0) {
							throw std::string("Invalid Day-of-Week value: '" + sub + "'");
						}
					}
					else if (c == '#') {
						i += 4;
						mNthDayOfWeek = boost::lexical_cast<int>(s.substr(i));
						if (mNthDayOfWeek < 1 || mNthDayOfWeek > 5) {
							throw std::string("A numeric value between 1 and 5 must follow the '#' option");
						}
					}
					else if (c == 'L') {
						mLastdayOfWeek = true;
						i++;
					}
				}

			}
			else {
				throw std::string("Illegal characters for this position: '" + sub + "'");
			}
			if (eval != -1) {
				incr = 1;
			}
			AddToSet(sval, eval, incr, type);
			return (i + 3);
		}

		if (c == '?') {
			i++;
			if ((i + 1) < s.length()
				&& (s[i] != ' ' && s[i + 1] != '\t')) {
				throw std::string("Illegal character after '?'");
			}
			if (type != DAY_OF_WEEK && type != DAY_OF_MONTH) {
				throw std::string("'?' can only be specified for Day-of-Month or Day-of-Week.");
			}
			if (type == DAY_OF_WEEK && !mLastdayOfMonth) {
				int val = *mDaysOfMonth.rbegin();
				if (val == NO_SPEC_INT) {
					throw std::string("'?' can only be specified for Day-of-Month -OR- Day-of-Week.");
				}
			}

			AddToSet(NO_SPEC_INT, -1, 0, type);
			return i;
		}

		if (c == '*' || c == '/') {
			if (c == '*' && (i + 1) >= s.length()) {
				AddToSet(ALL_SPEC_INT, -1, incr, type);
				return i + 1;
			}
			else if (c == '/'
				&& ((i + 1) >= s.length() || s[i + 1] == ' ' || s [i + 1] == '\t')) {
				throw std::string("'/' must be followed by an integer.");
			}
			else if (c == '*') {
				i++;
			}
			c = s[i];
			if (c == '/') { // is an increment specified?
				i++;
				if (i >= s.length()) {
					throw std::string("Unexpected end of string.");
				}

				incr = getNumericValue(s, i);

				i++;
				if (incr > 10) {
					i++;
				}
				CheckIncrementRange(incr, type, i);
			}
			else {
				incr = 1;
			}

			AddToSet(ALL_SPEC_INT, -1, incr, type);
			return i;
		}
		else if (c == 'L') {
			i++;
			if (type == DAY_OF_MONTH) {
				mLastdayOfMonth = true;
			}
			if (type == DAY_OF_WEEK) {
				AddToSet(7, 7, 0, type);
			}
			if (type == DAY_OF_MONTH && s.length() > i) {
				c = s[i];
				if (c == '-') {
					ValueSetPtr vs = getValue(0, s, i + 1);
					mLastdayOffset = vs->value;
					if (mLastdayOffset > 30)
						throw std::string("Offset from last day must be <= 30", i + 1);
					i = vs->pos;
				}
				if (s.length() > i) {
					c = s[i];
					if (c == 'W') {
						mNearestWeekday = true;
						i++;
					}
				}
			}
			return i;
		}
		else if (c >= '0' && c <= '9') {
			int val = boost::lexical_cast<int>(c);
			i++;
			if (i >= s.length()) {
				AddToSet(val, -1, -1, type);
			}
			else {
				c = s[i];
				if (c >= '0' && c <= '9') {
					ValueSetPtr vs = getValue(val, s, i);
					val = vs->value;
					i = vs->pos;
				}
				i = CheckNext(i, s, val, type);
				return i;
			}
		}
		else {
			throw std::string("Unexpected character: " + c);
		}

		return i;
	}

	int CronExpression::CheckNext(int pos, const std::string& s, int val, int type) {

		int end = -1;
		int i = pos;

		if (i >= s.length()) {
			AddToSet(val, end, -1, type);
			return i;
		}

		char c = s[pos];

		if (c == 'L') {
			if (type == DAY_OF_WEEK) {
				if (val < 1 || val > 7)
					throw std::string("Day-of-Week values must be between 1 and 7");
				mLastdayOfWeek = true;
			}
			else {
				throw std::string("'L' option is not valid here.");
			}
			auto& set = GetSet(type);
			set.insert(val);
			i++;
			return i;
		}

		if (c == 'W') {
			if (type == DAY_OF_MONTH) {
				mNearestWeekday = true;
			}
			else {
				throw std::string("'W' option is not valid here.");
			}
			if (val > 31)
				throw std::string("The 'W' option does not make sense with values larger than 31 (max number of days in a month)");
			auto& set = GetSet(type);
			set.insert(val);
			i++;
			return i;
		}

		if (c == '#') {
			if (type != DAY_OF_WEEK) {
				throw std::string("'#' option is not valid here.");
			}
			i++;
			mNthDayOfWeek = boost::lexical_cast<int>(s.substr(i));
			if (mNthDayOfWeek < 1 || mNthDayOfWeek > 5) {
				throw std::string("A numeric value between 1 and 5 must follow the '#' option");
			}

			auto& set = GetSet(type);
			set.insert(val);
			i++;
			return i;
		}

		if (c == '-') {
			i++;
			c = s[i];
			int v = boost::lexical_cast<int>(c);
			end = v;
			i++;
			if (i >= s.length()) {
				AddToSet(val, end, 1, type);
				return i;
			}
			c = s[i];
			if (c >= '0' && c <= '9') {
				ValueSetPtr vs = getValue(v, s, i);
				end = vs->value;
				i = vs->pos;
			}
			if (i < s.length() && ((c = s[i]) == '/')) {
				i++;
				c = s[i];
				int v2 = boost::lexical_cast<int>(c);
				i++;
				if (i >= s.length()) {
					AddToSet(val, end, v2, type);
					return i;
				}
				c = s[i];
				if (c >= '0' && c <= '9') {
					ValueSetPtr vs = getValue(v2, s, i);
					int v3 = vs->value;
					AddToSet(val, end, v3, type);
					i = vs->pos;
					return i;
				}
				else {
					AddToSet(val, end, v2, type);
					return i;
				}
			}
			else {
				AddToSet(val, end, 1, type);
				return i;
			}
		}

		if (c == '/') {
			if ((i + 1) >= s.length() || s[i + 1] == ' ' || s[i + 1] == '\t') {
				throw std::string("'/' must be followed by an integer.");
			}

			i++;
			c = s[i];
			int v2 = boost::lexical_cast<int>(c);
			i++;
			if (i >= s.length()) {
				CheckIncrementRange(v2, type, i);
				AddToSet(val, end, v2, type);
				return i;
			}
			c = s[i];
			if (c >= '0' && c <= '9') {
				ValueSetPtr vs = getValue(v2, s, i);
				int v3 = vs->value;
				CheckIncrementRange(v3, type, i);
				AddToSet(val, end, v3, type);
				i = vs->pos;
				return i;
			}
			else {
				throw std::string("Unexpected character after '/'");
			}
		}
		AddToSet(val, end, 0, type);
		i++;
		return i;
	}

	gsf::DateTimePtr CronExpression::GetTimeAfter(DateTime afterTime)
	{
		auto cl = std::make_shared<DateTime>();
		afterTime.AddSeconds(1);
		*cl = afterTime;

		auto gotOne = false;

		while (!gotOne) {

			//if (endTime != null && cl.getTime().after(endTime)) return null;
			if (cl->GetYear() > 2999) { // prevent endless loop...
				return nullptr;
			}

			std::set<int> st;
			int t = 0;

			int sec = cl->GetSeconds();
			int min = cl->GetMinutes();

			// get second.................................................
			st = tailSet(mSeconds, sec);
			if (!st.empty()) {
				sec = *st.begin();
			}
			else {
				sec = *mSeconds.begin();
				min++;
				cl->SetMinutes(min);
			}
			cl->SetSeconds(sec);

			min = cl->GetMinutes();
			int hr = cl->GetHoursOfDay();
			t = -1;

			// get minute.................................................
			st = tailSet(mMinutes, min);
			if (!st.empty()) {
				t = min;
				min = *st.begin();
			}
			else {
				min = *mMinutes.begin();
				hr++;
			}
			if (min != t) {
				cl->SetSeconds(0);
				cl->SetMinutes( min);
				cl->SetCalendarHour(hr);
				continue;
			}
			cl->SetMinutes( min);

			hr = cl->GetHoursOfDay();
			int day = cl->GetDaysOfMonth();
			t = -1;

			// get hour...................................................
			st = tailSet(mHours, hr);
			if (!st.empty()) {
				t = hr;
				hr = *st.begin();
			}
			else {
				hr = *mHours.begin();
				day++;
			}
			if (hr != t) {
				cl->SetSeconds(0);
				cl->SetMinutes(0);
				cl->SetDaysOfMonth(day);
				cl->SetCalendarHour(hr);
				continue;
			}
			cl->SetHoursOfDay(hr);

			day = cl->GetDaysOfMonth();
			int mon = cl->GetMonth() + 1;
			// '+ 1' because calendar is 0-based for this field, and we are
			// 1-based
			t = -1;
			int tmon = mon;

			// get day...................................................

			auto dayOfMSpec = mDaysOfMonth.find(NO_SPEC) == mDaysOfMonth.end();
			auto dayOfWSpec = mDaysOfWeek.find(NO_SPEC) == mDaysOfWeek.end();
			if (dayOfMSpec && !dayOfWSpec) { // get day by day of month rule
				st = tailSet(mDaysOfMonth, day);
				if (mLastdayOfMonth) {
					if (!mNearestWeekday) {
						t = day;
						day = getLastDayOfMonth(mon, cl->GetYear());
						day -= mLastdayOffset;
						if (t > day) {
							mon++;
							if (mon > 12) {
								mon = 1;
								tmon = 3333; // ensure test of mon != tmon further below fails
								cl->AddYear(1);
							}
							day = 1;
						}
					}
					else {
						t = day;
						day = getLastDayOfMonth(mon, cl->GetYear());
						day -= mLastdayOffset;

						auto tcal = std::make_shared<DateTime>();
						tcal->SetSeconds(0);
						tcal->SetMinutes(0);
						tcal->SetHoursOfDay(0);
						tcal->SetDaysOfMonth(day);
						tcal->SetMonth(mon - 1);
						tcal->SetYear(cl->GetYear());

						int ldom = getLastDayOfMonth(mon, cl->GetYear());
						int dow = tcal->GetDaysOfWeek();

						if (dow == SATURDAY && day == 1) {
							day += 2;
						}
						else if (dow == SATURDAY) {
							day -= 1;
						}
						else if (dow == SUNDAY && day == ldom) {
							day -= 2;
						}
						else if (dow == SUNDAY) {
							day += 1;
						}

						tcal->SetSeconds(sec);
						tcal->SetMinutes(min);
						tcal->SetHoursOfDay(hr);
						tcal->SetDaysOfMonth(day);
						tcal->SetMonth(mon - 1);

						DateTime nTime;
						if (nTime.Before(afterTime)) {
							day = 1;
							mon++;
						}
					}
				}
				else if (mNearestWeekday) {
					t = day;
					day = *mDaysOfMonth.begin();

					auto tcal = std::make_shared<DateTime>();
					tcal->SetSeconds(0);
					tcal->SetMinutes(0);
					tcal->SetHoursOfDay(0);
					tcal->SetDaysOfMonth(day);
					tcal->SetMonth(mon - 1);
					tcal->SetYear(cl->GetYear());


					int ldom = getLastDayOfMonth(mon, cl->GetYear());
					int dow = tcal->GetDaysOfWeek();

					if (dow == SATURDAY && day == 1) {
						day += 2;
					}
					else if (dow == SATURDAY) {
						day -= 1;
					}
					else if (dow == SUNDAY && day == ldom) {
						day -= 2;
					}
					else if (dow == SUNDAY) {
						day += 1;
					}


					tcal->SetSeconds(sec);
					tcal->SetMinutes(min);
					tcal->SetHoursOfDay(hr);
					tcal->SetDaysOfMonth(day);
					tcal->SetMonth(mon - 1);
					DateTime nTime;
					if (nTime.Before(afterTime)) {
						day = *mDaysOfMonth.begin();
						mon++;
					}
				}
				else if (!st.empty()) {
					t = day;
					day = *st.begin();
					// make sure we don't over-run a short month, such as february
					int lastDay = getLastDayOfMonth(mon, cl->GetYear());
					if (day > lastDay) {
						day = *mDaysOfMonth.begin();
						mon++;
					}
				}
				else {
					day = *mDaysOfMonth.begin();
					mon++;
				}

				if (day != t || mon != tmon) {
					cl->SetSeconds(0);
					cl->SetMinutes(0);
					cl->SetHoursOfDay(0);
					cl->SetDaysOfMonth(day);
					cl->SetMonth(mon - 1);
					// '- 1' because calendar is 0-based for this field, and we
					// are 1-based
					continue;
				}
			}
			else if (dayOfWSpec && !dayOfMSpec) { // get day by day of week rule
				if (mLastdayOfWeek) { // are we looking for the last XXX day of
					// the month?
					int dow = *mDaysOfWeek.begin(); // desired
					// d-o-w
					int cDow = cl->GetDaysOfWeek(); // current d-o-w
					int daysToAdd = 0;
					if (cDow < dow) {
						daysToAdd = dow - cDow;
					}
					if (cDow > dow) {
						daysToAdd = dow + (7 - cDow);
					}

					int lDay = getLastDayOfMonth(mon, cl->GetYear());

					if (day + daysToAdd > lDay) { // did we already miss the
						// last one?
						cl->SetSeconds(0);
						cl->SetMinutes(0);
						cl->SetHoursOfDay(0);
						cl->SetDaysOfMonth(1);
						cl->SetMonth(mon);
						// no '- 1' here because we are promoting the month
						continue;
					}

					// find date of last occurrence of this day in this month...
					while ((day + daysToAdd + 7) <= lDay) {
						daysToAdd += 7;
					}

					day += daysToAdd;

					if (daysToAdd > 0) {
						cl->SetSeconds(0);
						cl->SetMinutes(0);
						cl->SetHoursOfDay(0);
						cl->SetDaysOfMonth(day);
						cl->SetMonth(mon - 1);
						// '- 1' here because we are not promoting the month
						continue;
					}

				}
				else if (mNthDayOfWeek != 0) {
					// are we looking for the Nth XXX day in the month?
					int dow = *mDaysOfWeek.begin(); // desired
					// d-o-w
					int cDow = cl->GetDaysOfWeek(); // current d-o-w
					int daysToAdd = 0;
					if (cDow < dow) {
						daysToAdd = dow - cDow;
					}
					else if (cDow > dow) {
						daysToAdd = dow + (7 - cDow);
					}

					auto dayShifted = false;
					if (daysToAdd > 0) {
						dayShifted = true;
					}

					day += daysToAdd;
					int weekOfMonth = day / 7;
					if (day % 7 > 0) {
						weekOfMonth++;
					}

					daysToAdd = (mNthDayOfWeek - weekOfMonth) * 7;
					day += daysToAdd;
					if (daysToAdd < 0
						|| day > getLastDayOfMonth(mon, cl->GetYear())) {

						cl->SetSeconds(0);
						cl->SetMinutes(0);
						cl->SetHoursOfDay(0);
						cl->SetDaysOfMonth(1);
						cl->SetMonth(mon);
						// no '- 1' here because we are promoting the month
						continue;
					}
					else if (daysToAdd > 0 || dayShifted) {
						cl->SetSeconds(0);
						cl->SetMinutes(0);
						cl->SetHoursOfDay(0);
						cl->SetDaysOfMonth(day);
						cl->SetMonth(mon - 1);
						// '- 1' here because we are NOT promoting the month
						continue;
					}
				}
				else {
					int cDow = cl->GetDaysOfWeek(); // current d-o-w
					int dow = *mDaysOfWeek.begin(); // desired
					// d-o-w
					st = tailSet(mDaysOfWeek, cDow);
					if (!st.empty()) {
						dow = *st.begin();
					}

					int daysToAdd = 0;
					if (cDow < dow) {
						daysToAdd = dow - cDow;
					}
					if (cDow > dow) {
						daysToAdd = dow + (7 - cDow);
					}

					int lDay = getLastDayOfMonth(mon, cl->GetYear());

					if (day + daysToAdd > lDay) { // will we pass the end of
						// the month?
						cl->SetSeconds(0);
						cl->SetMinutes(0);
						cl->SetHoursOfDay(0);
						cl->SetDaysOfMonth(1);
						cl->SetMonth(mon);
						// no '- 1' here because we are promoting the month
						continue;
					}
					else if (daysToAdd > 0) { // are we swithing days?
						cl->SetSeconds(0);
						cl->SetMinutes(0);
						cl->SetHoursOfDay(0);
						cl->SetDaysOfMonth(day + daysToAdd);
						cl->SetMonth(mon - 1);
						// '- 1' because calendar is 0-based for this field,
						// and we are 1-based
						continue;
					}
				}
			}
			else { // dayOfWSpec && !dayOfMSpec
				throw std::string("Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.");
			}
			cl->SetDaysOfMonth(day);

			mon = cl->GetMonth() + 1;
			// '+ 1' because calendar is 0-based for this field, and we are
			// 1-based
			int year = cl->GetYear();
			t = -1;

			// test for expressions that never generate a valid fire date,
			// but keep looping...
			if (year > MAX_YEAR) {
				return nullptr;
			}

			// get month...................................................
			st = tailSet(mMonths, mon);
			if (!st.empty()) {
				t = mon;
				mon = *st.begin();
			}
			else {
				mon = *mMonths.begin();
				year++;
			}
			if (mon != t) {
				cl->SetSeconds(0);
				cl->SetMinutes(0);
				cl->SetHoursOfDay(0);
				cl->SetDaysOfMonth(1);
				cl->SetMonth(mon - 1);
				// '- 1' because calendar is 0-based for this field, and we are
				// 1-based
				cl->SetYear(year);
				continue;
			}
			cl->SetMonth(mon - 1);
			// '- 1' because calendar is 0-based for this field, and we are
			// 1-based

			year = cl->GetYear();
			t = -1;

			// get year...................................................
			st = tailSet(mYears, year);
			if (!st.empty()) {
				t = year;
				year = *st.begin();
			}
			else {
				return nullptr; // ran out of years...
			}

			if (year != t) {
				cl->SetSeconds(0);
				cl->SetMinutes(0);
				cl->SetHoursOfDay(0);
				cl->SetDaysOfMonth(1);
				cl->SetMonth(0);
				// '- 1' because calendar is 0-based for this field, and we are
				// 1-based
				cl->SetYear(year);
				continue;
			}
			cl->SetYear(year);

			gotOne = true;
		} // while( !done )

		return cl;
	}

	void CronExpression::BuildExpression()
	{
		mSeconds.clear();
		mMinutes.clear();
		mHours.clear();
		mDaysOfMonth.clear();
		mDaysOfWeek.clear();
		mMonths.clear();
		mYears.clear();

		int exprOn = SECOND;

		boost::char_separator<char> sep(" \t");
		boost::tokenizer<boost::char_separator<char>> tokens(mCronExpression, sep);
		for (auto& tok : tokens) {
			auto expr = tok;
			
			// throw an exception if L is used with other days of the month
			if (exprOn == DAY_OF_MONTH && expr.find('L') != std::string::npos && expr.length() > 1 && expr.find(',') != std::string::npos) {
				throw std::string("Support for specifying 'L' and 'LW' with other days of the month is not implemented");
			}
			// throw an exception if L is used with other days of the week
			if (exprOn == DAY_OF_WEEK && expr.find('L') != std::string::npos && expr.length() > 1 && expr.find(',') != std::string::npos) {
				throw std::string("Support for specifying 'L' with other days of the week is not implemented");
			}
			if (exprOn == DAY_OF_WEEK && expr.find('#') != std::string::npos && expr.find('#', expr.find('#') + 1) != std::string::npos) {
				throw std::string("Support for specifying multiple \"nth\" days is not implemented.");
			}

			boost::char_separator<char> sep2(",");
			boost::tokenizer<boost::char_separator<char>> tokens2(expr, sep2);
			for (auto& tok2 : tokens2) {
				StoreExpressionVals(0, tok2, exprOn);
			}

			++exprOn;
		}

		if (exprOn <= DAY_OF_WEEK) {
			throw std::string("Unexpected end of expression.");
		}

		if (exprOn <= YEAR) {
			StoreExpressionVals(0, "*", YEAR);
		}

		auto& dow = GetSet(DAY_OF_WEEK);
		auto& dom = GetSet(DAY_OF_MONTH);

		// Copying the logic from the UnsupportedOperationException below
		auto dayOfMSpec = dom.find(NO_SPEC) == dom.end();
		auto dayOfWSpec = dow.find(NO_SPEC) == dow.end();

		if (!dayOfMSpec || dayOfWSpec) {
			if (!dayOfWSpec || dayOfMSpec) {
				throw std::string("Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.");
			}
		}
	}

	void CronExpression::AddToSet(int val, int end, int incr, int type) {
		auto& set = GetSet(type);

		if (type == SECOND || type == MINUTE) {
			if ((val < 0 || val > 59 || end > 59) && (val != ALL_SPEC_INT)) {
				throw std::string("Minute and Second values must be between 0 and 59");
			}
		}
		else if (type == HOUR) {
			if ((val < 0 || val > 23 || end > 23) && (val != ALL_SPEC_INT)) {
				throw std::string("Hour values must be between 0 and 23");
			}
		}
		else if (type == DAY_OF_MONTH) {
			if ((val < 1 || val > 31 || end > 31) && (val != ALL_SPEC_INT)
				&& (val != NO_SPEC_INT)) {
				throw std::string("Day of month values must be between 1 and 31");
			}
		}
		else if (type == MONTH) {
			if ((val < 1 || val > 12 || end > 12) && (val != ALL_SPEC_INT)) {
				throw std::string("Month values must be between 1 and 12");
			}
		}
		else if (type == DAY_OF_WEEK) {
			if ((val == 0 || val > 7 || end > 7) && (val != ALL_SPEC_INT)
				&& (val != NO_SPEC_INT)) {
				throw std::string("Day-of-Week values must be between 1 and 7");
			}
		}

		if ((incr == 0 || incr == -1) && val != ALL_SPEC_INT) {
			if (val != -1) {
				set.insert(val);
			}
			else {
				set.insert(NO_SPEC);
			}

			return;
		}

		int startAt = val;
		int stopAt = end;

		if (val == ALL_SPEC_INT && incr <= 0) {
			incr = 1;
			set.insert(ALL_SPEC); // put in a marker, but also fill values
		}

		if (type == SECOND || type == MINUTE) {
			if (stopAt == -1) {
				stopAt = 59;
			}
			if (startAt == -1 || startAt == ALL_SPEC_INT) {
				startAt = 0;
			}
		}
		else if (type == HOUR) {
			if (stopAt == -1) {
				stopAt = 23;
			}
			if (startAt == -1 || startAt == ALL_SPEC_INT) {
				startAt = 0;
			}
		}
		else if (type == DAY_OF_MONTH) {
			if (stopAt == -1) {
				stopAt = 31;
			}
			if (startAt == -1 || startAt == ALL_SPEC_INT) {
				startAt = 1;
			}
		}
		else if (type == MONTH) {
			if (stopAt == -1) {
				stopAt = 12;
			}
			if (startAt == -1 || startAt == ALL_SPEC_INT) {
				startAt = 1;
			}
		}
		else if (type == DAY_OF_WEEK) {
			if (stopAt == -1) {
				stopAt = 7;
			}
			if (startAt == -1 || startAt == ALL_SPEC_INT) {
				startAt = 1;
			}
		}
		else if (type == YEAR) {
			if (stopAt == -1) {
				stopAt = MAX_YEAR;
			}
			if (startAt == -1 || startAt == ALL_SPEC_INT) {
				startAt = 1970;
			}
		}

		// if the end of the range is before the start, then we need to overflow into 
		// the next day, month etc. This is done by adding the maximum amount for that 
		// type, and using modulus max to determine the value being added.
		int max = -1;
		if (stopAt < startAt) {
			switch (type) {
			case       SECOND: max = 60; break;
			case       MINUTE: max = 60; break;
			case         HOUR: max = 24; break;
			case        MONTH: max = 12; break;
			case  DAY_OF_WEEK: max = 7;  break;
			case DAY_OF_MONTH: max = 31; break;
			case         YEAR: throw std::string("Start year must be less than stop year");
			default: throw std::string("Unexpected type encountered");
			}
			stopAt += max;
		}

		for (int i = startAt; i <= stopAt; i += incr) {
			if (max == -1) {
				// ie: there's no max to overflow over
				set.insert(i);
			}
			else {
				// take the modulus to get the real value
				int i2 = i % max;

				// 1-indexed ranges should not include 0, and should include their max
				if (i2 == 0 && (type == MONTH || type == DAY_OF_WEEK || type == DAY_OF_MONTH)) {
					i2 = max;
				}

				set.insert(i2);
			}
		}
	}

	DateTime::DateTime()
	{
		memset(&mTM, 0, sizeof(mTM));
		time(&mTime);
#ifdef _WIN32
		_localtime64_s(&mTM, &mTime);
#else
		localtime_r(&mTime, &mTM);
#endif
	}

	DateTime::DateTime(const DateTime& d)
	{
		mTime = d.mTime;
#ifdef _WIN32
		_localtime64_s(&mTM, &mTime);
#else
		localtime_r(&mTime, &mTM);
#endif
	}

	void DateTime::OnChanged()
	{
#ifdef _WIN32
		mTime = _mktime64(&mTM);
#else
		mTime = mktime(&mTM);
#endif		
	}

	int DateTime::GetYear()
	{
		return mTM.tm_year + 1900;
	}

	int DateTime::GetMonth()
	{
		return mTM.tm_mon;
	}

	int DateTime::GetDaysOfMonth()
	{
		return mTM.tm_mday;
	}

	int DateTime::GetDaysOfWeek()
	{
		return mTM.tm_wday + 1;
	}

	int DateTime::GetHoursOfDay()
	{
		return mTM.tm_hour;
	}

	int DateTime::GetMinutes()
	{
		return mTM.tm_min;
	}

	int DateTime::GetSeconds()
	{
		return mTM.tm_sec;
	}

	int64_t DateTime::GetTotalSeconds()
	{
		return mTime;
	}

	int64_t DateTime::GetTotalMilliSeconds()
	{
		return mTime * 1000;
	}

	void DateTime::SetYear(int v)
	{
		auto olastday = getLastDayOfMonth(mTM.tm_mon + 1, GetYear());
		auto nlastday = getLastDayOfMonth(mTM.tm_mon + 1, v);

		mTM.tm_year = v - 1900;
		if (mTM.tm_mday > nlastday) {
			mTM.tm_mday = mTM.tm_mday - nlastday;
			SetMonth(mTM.tm_mon + 1);
		}
		else
		{
			OnChanged();
		}
	}

	void DateTime::SetMonth(int v)
	{
		auto olastday = getLastDayOfMonth(mTM.tm_mon + 1, GetYear());
		auto nlastday = 0;
		if (v > 11)
		{
			nlastday = getLastDayOfMonth((v % 12) + 1, GetYear() + (v / 12));
			mTM.tm_year += (v / 12);
			v = (v % 12);
		}
		else
		{
			nlastday = getLastDayOfMonth(v + 1, GetYear());
		}

		mTM.tm_mon = v;

		if (mTM.tm_mday > nlastday) {
			mTM.tm_mday = mTM.tm_mday - nlastday;
			SetMonth(mTM.tm_mon + 1);
		}
		else
		{
			OnChanged();
		}
	}

	void DateTime::SetDaysOfMonth(int v)
	{
		auto lastday = getLastDayOfMonth(mTM.tm_mon + 1, GetYear());
		if (v > lastday) {
			mTM.tm_mday = v % lastday;
			SetMonth(mTM.tm_mon + 1);
		}
		else 
		{
			mTM.tm_mday = v;
			OnChanged();
		}
	}

	void DateTime::SetHoursOfDay(int v)
	{
		if (v >= 24) {
			mTM.tm_hour = v % 24;
			SetDaysOfMonth(mTM.tm_mday + (v / 24));
		}
		else
		{
			mTM.tm_hour = v;
			OnChanged();
		}
	}

	void DateTime::SetMinutes(int v)
	{
		if (v >= 60) {
			mTM.tm_min = v % 60;
			SetHoursOfDay(mTM.tm_hour + (v / 60));
		}
		else
		{
			mTM.tm_min = v;
			OnChanged();
		}
	}

	void DateTime::SetSeconds(int v)
	{
		if (v >= 60) {
			mTM.tm_sec = v % 60;
			SetMinutes(mTM.tm_min + (v / 60));
		}
		else
		{
			mTM.tm_sec = v;
			OnChanged();
		}
	}

	void DateTime::AddSeconds(int diff)
	{
		SetSeconds(GetSeconds() + diff);
	}

	void DateTime::AddYear(int y)
	{
		SetYear(GetYear() + y);
	}

	bool DateTime::Before(const DateTime& d)
	{
		return mTime < d.mTime;
	}

	std::string DateTime::ToString()
	{
		std::stringstream ss;
		ss << GetYear() << "-" << (GetMonth() + 1) << "-" << GetDaysOfMonth() << " " << GetHoursOfDay() << ":" << GetMinutes() << ":" << GetSeconds() << " t=" << mTime;
		return ss.str();
	}

}